Skip to content

fix(flow): apply deferred narrows after antecedent resolution#989

Open
lewis6991 wants to merge 4 commits intoEmmyLuaLs:mainfrom
lewis6991:fix/returnoverloadrecursion
Open

fix(flow): apply deferred narrows after antecedent resolution#989
lewis6991 wants to merge 4 commits intoEmmyLuaLs:mainfrom
lewis6991:fix/returnoverloadrecursion

Conversation

@lewis6991
Copy link
Collaborator

@lewis6991 lewis6991 commented Mar 20, 2026

When walking backward through flow, collect condition narrows as pending
actions instead of applying them while recursively querying antecedent
types. The eager path mixed narrowing with antecedent resolution, so
stacked guards could re-enter flow inference from inside condition
evaluation and build deep recursive chains across repeated truthiness,
type-guard, signature-cast, and correlated return-overload checks.

Resolve the antecedent type first, then apply the pending narrows in
reverse order. That keeps narrowing as a post-pass over an
already-resolved input, avoids re-entering the same condition chain
while answering the current flow query, and lets same-variable
self/member guards wait until earlier guards have narrowed the receiver
enough for reliable lookup.

Key the flow cache by whether condition narrowing is enabled, and
separate assignment source lookup from condition application. Reuse a
narrowed source only when the RHS preserves that precision; broader RHS
expressions fall back to the antecedent type with condition narrowing
disabled so reassignment clears stale branch narrows, while exact
literals and compatible partial table/object rewrites still preserve
useful narrowing.

Add regression coverage for stacked guards, correlated overload joins,
pcall aliases, and assign/return diagnostics.


These added tests fail on main:

  • test_stacked_same_var_type_guards_build_semantic_model
  • test_stacked_same_field_truthiness_guards_build_semantic_model
  • test_stacked_return_cast_guards_build_semantic_model
  • test_stacked_return_cast_self_guards_build_semantic_model
  • test_assignment_from_wider_single_return_call_drops_branch_narrowing
  • test_assignment_after_pending_return_cast_guard_drops_branch_narrowing
  • test_assignment_after_binary_call_guard_eq_false_drops_branch_narrowing
  • test_assignment_after_mixed_eager_and_pending_guards_drops_branch_narrowing
  • test_assignment_from_nullable_union_keeps_rhs_members
  • test_assignment_from_partially_overlapping_union_keeps_rhs_members
  • test_assignment_from_broad_string_drops_literal_branch_narrowing
  • test_partial_table_reassignment_with_conflicting_discriminant_drops_branch_narrowing
  • test_pcall_stacked_alias_guards_build_semantic_model
  • test_return_overload_stacked_eq_guards_build_semantic_model
  • test_return_overload_stacked_mixed_guards_build_semantic_model
  • test_return_overload_uncorrelated_later_guard_keeps_prior_narrowing
  • test_return_overload_unmatched_target_call_keeps_guard_union
  • test_return_overload_unmatched_target_root_then_truthiness_guard
  • test_return_overload_unmatched_target_root_then_type_guard
  • test_return_overload_post_guard_reassign_clears_mixed_root_narrowing
  • test_return_overload_branch_reassign_to_different_call_preserves_matching_root_narrowing
  • test_return_overload_branch_reassign_to_different_call_narrows_alternate_matching_root

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical recursion bug within the type inference system, specifically affecting correlated @return_overload narrowing. By introducing a guard mechanism, it prevents infinite loops and stack overflows during complex type analysis, significantly enhancing the stability and reliability of the semantic model generation without altering the core inference logic for valid cases.

Highlights

  • Addressed recursive re-entry in @return_overload narrowing: Fixed an issue where correlated @return_overload narrowing could recursively re-enter itself for the same target return slot, leading to stack overflows or hung indexing due to a missing in-flight recursion break.
  • Introduced a recursion guard: Implemented a correlated_condition_guard (a HashSet) within LuaInferCache to prevent re-entry into narrow_var_from_return_overload_condition for an already in-flight correlated query.
  • Improved type inference stability: When re-entry is detected by the guard, the inference process now returns Continue, allowing the flow walk to fall back to non-correlated inference instead of repeating the problematic correlated query.
  • Added a regression test: Included a new regression test case that specifically reproduces the stack overflow issue with many repeated if-not-ok guards, confirming the fix allows the semantic model to build correctly.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly identifies and fixes a potential stack overflow issue caused by recursive re-entry in correlated @return_overload narrowing. The approach of adding a guard to LuaInferCache is sound. The added regression test is also a great way to ensure this issue does not reappear. I have one suggestion to improve the robustness of the re-entrancy guard implementation.

@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from efa1423 to 81ac6be Compare March 20, 2026 12:46
@lewis6991 lewis6991 marked this pull request as draft March 20, 2026 12:58
@lewis6991 lewis6991 marked this pull request as ready for review March 20, 2026 13:10
@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from 81ac6be to 231495e Compare March 20, 2026 16:31
@lewis6991 lewis6991 marked this pull request as draft March 20, 2026 16:36
@lewis6991 lewis6991 changed the title fix(flow): guard correlated return-overload reentry fix(flow): defer stacked condition narrowing during flow walks Mar 20, 2026
@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from 231495e to efc8d41 Compare March 20, 2026 22:18
@lewis6991 lewis6991 changed the title fix(flow): defer stacked condition narrowing during flow walks fix(flow): stop re-entering get_type_at_flow from condition narrows Mar 20, 2026
@lewis6991 lewis6991 marked this pull request as ready for review March 20, 2026 22:19
@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch 2 times, most recently from 6d04cf4 to bf14cab Compare March 20, 2026 23:15
@clason

This comment was marked as resolved.

@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from bf14cab to 2054627 Compare March 21, 2026 17:42
@lewis6991 lewis6991 changed the title fix(flow): stop re-entering get_type_at_flow from condition narrows fix(flow): replay deferred condition narrows after antecedent lookup Mar 21, 2026
@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch 2 times, most recently from 3fa1774 to 7ad7200 Compare March 22, 2026 00:59
@lewis6991 lewis6991 changed the title fix(flow): replay deferred condition narrows after antecedent lookup fix(flow): apply deferred narrows after antecedent resolution Mar 22, 2026
@lewis6991
Copy link
Collaborator Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly refactors the control flow analysis to defer type narrowing. By collecting narrowing actions and applying them after resolving the antecedent type, you've addressed potential deep recursion issues and improved the correctness of type inference, especially for complex scenarios involving stacked guards and reassignments. The changes are substantial and well-supported by an impressive suite of new regression tests, which gives great confidence in the robustness of this new approach. The implementation of pending narrows and the updated caching strategy are well-executed. Overall, this is a high-quality improvement to the type system's core logic.

@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from 7ad7200 to e102572 Compare March 23, 2026 12:48
right_expr: LuaExpr,
condition_flow: InferConditionFlow,
) -> Result<ResultTypeOrContinue, InferFailReason> {
) -> Result<Option<ConditionFlowAction>, InferFailReason> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using "option" here instead of "continue" somewhat reduces readability.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use option here because try_get_at_eq_or_neq_expr has:

    if let Some(action) = maybe_type_guard_binary_action(
        db,
        tree,
        cache,
        root,
        var_ref_id,
        flow_node,
        left_expr.clone(),
        right_expr.clone(),
        condition_flow,
    )? {
        return Ok(action);
    }

Using continue would make this a little worse I think.

@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from e102572 to d8e5882 Compare March 26, 2026 08:46
}

fn get_single_antecedent(tree: &FlowTree, flow: &FlowNode) -> Result<FlowId, InferFailReason> {
fn get_single_antecedent(_tree: &FlowTree, flow: &FlowNode) -> Result<FlowId, InferFailReason> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also remove, Many places no longer use FlowTree and use only FlowNode — does that mean they've abandoned having multiple decision paths?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

The multiple-path flow model is still intact. FlowAntecedent::Multiple is still represented in the CFG, expanded via get_multi_antecedents, and used at branch joins in get_type_at_flow. get_single_antecedent is only the linear-predecessor helper, so it intentionally rejects Multiple.

When walking backward through flow, collect condition narrows as pending
actions instead of applying them while recursively querying antecedent
types. The eager path mixed narrowing with antecedent resolution, so
stacked guards could re-enter flow inference from inside condition
evaluation and build deep recursive chains across repeated truthiness,
type-guard, signature-cast, and correlated return-overload checks.

Resolve the antecedent type first, then apply the pending narrows in
reverse order. That keeps narrowing as a post-pass over an
already-resolved input, avoids re-entering the same condition chain
while answering the current flow query, and lets same-variable
self/member guards wait until earlier guards have narrowed the receiver
enough for reliable lookup.

Key the flow cache by whether condition narrowing is enabled, and
separate assignment source lookup from condition application. Reuse a
narrowed source only when the RHS preserves that precision; broader RHS
expressions fall back to the antecedent type with condition narrowing
disabled so reassignment clears stale branch narrows, while exact
literals and compatible partial table/object rewrites still preserve
useful narrowing.

Add regression coverage for stacked guards, correlated overload joins,
pcall aliases, and assign/return diagnostics.
@lewis6991 lewis6991 force-pushed the fix/returnoverloadrecursion branch from d8e5882 to 76a7a8e Compare March 26, 2026 09:29
@CppCXY
Copy link
Member

CppCXY commented Mar 26, 2026

Is there any performance degradation or improvement when processing a single large file—for example, a definition file with many assignment statements of about 10k lines?

@lewis6991
Copy link
Collaborator Author

The motivation for this change was to improve performance by reducing recursion. The return_overload patch pushed the current flow analysis to its limits and I found a case where it caused indexing to hang.

To be extra sure, I'll run this on a few projects and get back to you.

@clason
Copy link
Contributor

clason commented Mar 26, 2026

...and also to fix a regression that prevents processing certain (i.e., my) projects ;) So the mentioned hang is not hypothetical.

@CppCXY
Copy link
Member

CppCXY commented Mar 26, 2026

...and also to fix a regression that prevents processing certain (i.e., my) projects ;) So the mentioned hang is not hypothetical.

I often receive reports of performance issues from the language server on large configuration tables and on config files with many assignment definitions; I’m aware of it, but I haven’t gotten around to fixing it.

@clason
Copy link
Contributor

clason commented Mar 26, 2026

No, this is not about a performance issue. This is a very specific regression introduced in #985 which makes analysis hang at 100% CPU for hours on nvim-treesitter. This PR fixes that regression so analysis completes quickly again.

@lewis6991
Copy link
Collaborator Author

...and also to fix a regression that prevents processing certain (i.e., my) projects ;) So the mentioned hang is not hypothetical.

I often receive reports of performance issues from the language server on large configuration tables and on config files with many assignment definitions; I’m aware of it, but I haven’t gotten around to fixing it.

I did add a stress regression test for a large linear assignment-heavy file, test_large_linear_assignment_file_builds_semantic_model, so CI now covers that shape as well. It is not a benchmark, but it should catch this refactor regressing back into pathological flow walking on long assignment chains.

@lewis6991
Copy link
Collaborator Author

lewis6991 commented Mar 26, 2026

I've tested indexing neovim, gitsigns and they index very quickly. I also tried my neovim config which includes all my plugins (~4000 files) and that indexed reasonably quickly too (3-6 seconds).

@xuhuanzy
Copy link
Member

The performance issues come from game development projects, where many projects have over ten thousand files and individual files are megabytes in size.
This is completely different from your usage environment, and I think it's hard for you to imagine the specific tests

@lewis6991
Copy link
Collaborator Author

Ok, then can you help me confirm whether this PR improves or decreases performance on such projects? I'm yet to see a project referenced in any issue.

@CppCXY
Copy link
Member

CppCXY commented Mar 26, 2026

lua-bench.zip
as a example file, By comparing load times with the latest release on GitHub, I found that it is noticeably twice as slow in diagnostics, while loading performance is probably about the same. This is likely the cumulative result of several PRs, although most of those PRs were submitted by you.

@CppCXY
Copy link
Member

CppCXY commented Mar 26, 2026

and I found the collectgarbage will become unknown at line: 9249
image
However, it probably has nothing to do with you, because this issue existed in earlier versions.

@lewis6991
Copy link
Collaborator Author

lewis6991 commented Mar 26, 2026

lua-bench.zip as a example file, By comparing load times with the latest release on GitHub, I found that it is noticeably twice as slow in diagnostics, while loading performance is probably about the same. This is likely the cumulative result of several PRs, although most of those PRs were submitted by you.

Thanks for this. We should probably add this to CI in some form so we can have some better control on performance regressions.

The last few commits do improve analysis quite a bit, and that obviously come at some cost, though now we have a reference project that can use help guide optimizations.

I'll check how this PR specially affects this workload. If it is the same or improves things then I hope we can merge this and add further optimizations in follow ups.

@lewis6991
Copy link
Collaborator Author

lua-bench.zip does the same recursion hang on main. With this PR diagnostics complete in about 2.5s on my machine using emmylua_check.

@lewis6991
Copy link
Collaborator Author

Latest release: 1.38s
This PR: 2.56s

So it does slow things down quite a bit. I'll try and work on optimizing that.

Add a dedicated condition-flow cache keyed by variable reference,
antecedent flow, and branch polarity so stacked guards can reuse earlier
results instead of re-entering the same narrowing path recursively.

Only build correlated return-overload narrows when both the discriminant
and target participate in multi-return tracking. That keeps ordinary
same-variable truthiness and equality guards on the direct narrowing
path while preserving correlated narrowing for real return-overload
cases.

Add a regression test for repeated `if not value then return end` guards
so the semantic model still builds and the narrowed type remains
`string`.
@lewis6991
Copy link
Collaborator Author

lewis6991 commented Mar 26, 2026

Added some caching.

Ran a quick local lua-bench comparison between current HEAD (60798f5c) and the latest release tag 0.21.0 (1546f82e) using emmylua_check lua- bench on lua-bench/10k_row_code.lua.

Method:

  • rebuilt both emmylua_check binaries locally with the same toolchain
  • did 1 warm-up run, then 5 timed warm-cache runs for each build
build runs (s) average
0.21.0 1.34, 1.32, 1.33, 1.34, 1.35 1.336s
HEAD 0.50, 0.54, 0.54, 0.51, 0.54 0.526s

On this workload, HEAD is about 2.5x faster than the release build.

@xuhuanzy
Copy link
Member

How is the memory usage? Has it increased significantly?
To be clear, game development is the top priority for this project, and we are very concerned about memory usage.

@lewis6991
Copy link
Collaborator Author

I don't know. It's most likely going to be better then main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants